﻿namespace FastColoredTextBoxNS
{
    using System;
    using System.Collections.Generic;

    public class CommandManager
    {
        private readonly int maxHistoryLength = 200;

        private readonly LimitedStack<UndoableCommand> history;

        private readonly Stack<UndoableCommand> redoStack = new Stack<UndoableCommand>();

        public TextSource TextSource { get; private set; }

        public bool UndoRedoStackIsEnabled { get; set; }

        public CommandManager(TextSource ts)
        {
            this.history = new LimitedStack<UndoableCommand>(this.maxHistoryLength);
            this.TextSource = ts;
            this.UndoRedoStackIsEnabled = true;
        }

        public virtual void ExecuteCommand(Command cmd)
        {
            if (this.disabledCommands > 0)
            {
                return;
            }

            //multirange ?
            if (cmd.ts.CurrentTB.Selection.ColumnSelectionMode)
            {
                if (cmd is UndoableCommand)
                {
                    //make wrapper
                    cmd = new MultiRangeCommand((UndoableCommand)cmd);
                }
            }

            if (cmd is UndoableCommand)
            {
                //if range is ColumnRange, then create wrapper
                (cmd as UndoableCommand).autoUndo = this.autoUndoCommands > 0;
                this.history.Push(cmd as UndoableCommand);
            }

            try
            {
                cmd.Execute();
            }
            catch (ArgumentOutOfRangeException)
            {
                //OnTextChanging cancels enter of the text
                if (cmd is UndoableCommand)
                {
                    this.history.Pop();
                }
            }
            //
            if (!this.UndoRedoStackIsEnabled)
            {
                this.ClearHistory();
            }
            //
            this.redoStack.Clear();
            //
            this.TextSource.CurrentTB.OnUndoRedoStateChanged();
        }

        public void Undo()
        {
            if (this.history.Count > 0)
            {
                UndoableCommand cmd = this.history.Pop();
                //
                this.BeginDisableCommands(); //prevent text changing into handlers
                try
                {
                    cmd.Undo();
                }
                finally
                {
                    this.EndDisableCommands();
                }
                //
                this.redoStack.Push(cmd);
            }

            //undo next autoUndo command
            if (this.history.Count > 0)
            {
                if (this.history.Peek().autoUndo)
                {
                    this.Undo();
                }
            }

            this.TextSource.CurrentTB.OnUndoRedoStateChanged();
        }

        protected int disabledCommands = 0;

        private void EndDisableCommands()
        {
            this.disabledCommands--;
        }

        private void BeginDisableCommands()
        {
            this.disabledCommands++;
        }

        private int autoUndoCommands;

        public void EndAutoUndoCommands()
        {
            this.autoUndoCommands--;
            if (this.autoUndoCommands == 0)
            {
                if (this.history.Count > 0)
                {
                    this.history.Peek().autoUndo = false;
                }
            }
        }

        public void BeginAutoUndoCommands()
        {
            this.autoUndoCommands++;
        }

        internal void ClearHistory()
        {
            this.history.Clear();
            this.redoStack.Clear();
            this.TextSource.CurrentTB.OnUndoRedoStateChanged();
        }

        internal void Redo()
        {
            if (this.redoStack.Count == 0)
            {
                return;
            }
            UndoableCommand cmd;
            this.BeginDisableCommands(); //prevent text changing into handlers
            try
            {
                cmd = this.redoStack.Pop();
                if (this.TextSource.CurrentTB.Selection.ColumnSelectionMode)
                {
                    this.TextSource.CurrentTB.Selection.ColumnSelectionMode = false;
                }
                this.TextSource.CurrentTB.Selection.Start = cmd.sel.Start;
                this.TextSource.CurrentTB.Selection.End = cmd.sel.End;
                cmd.Execute();
                this.history.Push(cmd);
            }
            finally
            {
                this.EndDisableCommands();
            }

            //redo command after autoUndoable command
            if (cmd.autoUndo)
            {
                this.Redo();
            }

            this.TextSource.CurrentTB.OnUndoRedoStateChanged();
        }

        public bool UndoEnabled
        {
            get
            {
                return this.history.Count > 0;
            }
        }

        public bool RedoEnabled
        {
            get
            {
                return this.redoStack.Count > 0;
            }
        }
    }

    public abstract class Command
    {
        public TextSource ts;

        public abstract void Execute();
    }

    internal class RangeInfo
    {
        public Place Start { get; set; }

        public Place End { get; set; }

        public RangeInfo(Range r)
        {
            this.Start = r.Start;
            this.End = r.End;
        }

        internal int FromX
        {
            get
            {
                if (this.End.iLine < this.Start.iLine)
                {
                    return this.End.iChar;
                }
                if (this.End.iLine > this.Start.iLine)
                {
                    return this.Start.iChar;
                }
                return Math.Min(this.End.iChar, this.Start.iChar);
            }
        }
    }

    public abstract class UndoableCommand : Command
    {
        internal RangeInfo sel;

        internal RangeInfo lastSel;

        internal bool autoUndo;

        public UndoableCommand(TextSource ts)
        {
            this.ts = ts;
            this.sel = new RangeInfo(ts.CurrentTB.Selection);
        }

        public virtual void Undo()
        {
            this.OnTextChanged(true);
        }

        public override void Execute()
        {
            this.lastSel = new RangeInfo(this.ts.CurrentTB.Selection);
            this.OnTextChanged(false);
        }

        protected virtual void OnTextChanged(bool invert)
        {
            bool b = this.sel.Start.iLine < this.lastSel.Start.iLine;
            if (invert)
            {
                if (b)
                {
                    this.ts.OnTextChanged(this.sel.Start.iLine, this.sel.Start.iLine);
                }
                else
                {
                    this.ts.OnTextChanged(this.sel.Start.iLine, this.lastSel.Start.iLine);
                }
            }
            else
            {
                if (b)
                {
                    this.ts.OnTextChanged(this.sel.Start.iLine, this.lastSel.Start.iLine);
                }
                else
                {
                    this.ts.OnTextChanged(this.lastSel.Start.iLine, this.lastSel.Start.iLine);
                }
            }
        }

        public abstract UndoableCommand Clone();
    }
}